Union Types in GoloScript

This comprehensive guide explains how to use union types in GoloScript: definition, syntax, common patterns, and augmentation.

Introduction

Union types are a powerful GoloScript feature that allows defining a type that can have multiple forms (variants). It’s similar to TypeScript union types or Rust enums, but with GoloScript’s dynamic flexibility.

Why Use Unions?

Basic Syntax

Defining a Union

union Result = {
  Success = { value }
  Failure = { error }
}

This definition creates:

Variants Without Fields

union Status = {
  Pending           # No fields
  Running = { pid }
  Done = { exitCode }
}

Three Constructor Syntaxes

GoloScript offers three different syntaxes for creating union instances. They’re all equivalent - choose the style that fits your code!

1. Dot Notation (Original Golo Syntax)

let result = Result.Success(42)
let status = Status.Pending()

Advantages: Clear, familiar for Java/JavaScript users

2. Underscore Notation

let result = Result_Failure("error message")
let status = Status_Running(1234)

Advantages: More concise, avoids conflicts with method calls

3. Method Call Notation

let result = Result: Success(100)
let status = Status: Done(0)

Advantages: Consistent with GoloScript method call syntax

Complete Example

# All these syntaxes are valid and equivalent:
let r1 = Result.Success(42)
let r2 = Result_Success(42)
let r3 = Result:Success(42)

# r1, r2, and r3 are identical!

Common Patterns

Pattern 1: Option Type

The Option type represents a value that may or may not exist. It’s a safer alternative to null.

union Option = { Some = { value }, None }

function safeDivide = |a, b| {
  if b == 0 {
    return Option_None()
  } else {
    return Option_Some(a / b)
  }
}

let result = safeDivide(10, 2)
if result: isSome() {
  println("Result: " + str(result: value()))
}

Use Cases: Operations that can fail, lookups, parsing

Pattern 2: Result Type

The Result type represents either success or failure with an error message.

union Result = { Ok = { value }, Err = { message } }

function validateAge = |age| {
  if age < 0 {
    return Result_Err("Age cannot be negative")
  } else if age > 150 {
    return Result_Err("Age too high")
  } else {
    return Result_Ok(age)
  }
}

let validation = validateAge(25)
if validation: isOk() {
  println("Valid age: " + str(validation: value()))
} else {
  println("Error: " + validation: message())
}

Use Cases: Validation, I/O operations, error handling

Pattern 3: Tree Structure

Unions are perfect for recursive data structures.

union Tree = { Empty, Node = { value, left, right } }

let tree = Tree_Node(
  10,
  Tree_Node(5, Tree_Empty(), Tree_Empty()),
  Tree_Node(15, Tree_Empty(), Tree_Empty())
)

function treeSize = |t| {
  if t: isEmpty() {
    return 0
  } else {
    let left = t: left()
    let right = t: right()
    return 1 + treeSize(left) + treeSize(right)
  }
}

println("Tree size: " + str(treeSize(tree)))  # 3

Use Cases: Binary trees, lists, AST expressions

Pattern 4: State Machine

Model the states and transitions of a state machine.

union TaskState = {
  Pending
  Running = { progress }
  Completed = { result }
  Failed = { error }
}

function describeTask = |state| {
  return match {
    when state: isPending() then "⏳ Pending"
    when state: isRunning() then "πŸ”„ Running (" + str(state: progress()) + "%)"
    when state: isCompleted() then "βœ… Completed: " + state: result()
    when state: isFailed() then "❌ Failed: " + state: error()
    otherwise "Unknown state"
  }
}

Use Cases: Workflows, async processes, UI states

Union Augmentation

Augmentation allows adding methods to union types, creating powerful object-oriented abstractions.

Basic Augmentation

union Option = { Some = { value }, None }

augment Option {
  function getOrDefault = |this, defaultValue| {
    if this: isSome() {
      return this: value()
    } else {
      return defaultValue
    }
  }

  function isDefined = |this| {
    return this: isSome()
  }
}

let some = Option_Some(42)
let none = Option_None()

println(some: getOrDefault(0))  # 42
println(none: getOrDefault(0))  # 0

Functional Methods

union Result = { Ok = { value }, Err = { message } }

augment Result {
  function map = |this, fn| {
    if this: isOk() {
      let val = this: value()
      return Result_Ok(fn(val))
    } else {
      return this
    }
  }

  function getOrDefault = |this, defaultValue| {
    if this: isOk() {
      return this: value()
    } else {
      return defaultValue
    }
  }
}

# Chaining operations
let result = Result_Ok(10)
  : map(|x| { return x * 2 })
  : map(|x| { return x + 10 })

println(result: getOrDefault(0))  # 30

Tree Augmentation

union Tree = { Empty, Node = { value, left, right } }

augment Tree {
  function size = |this| {
    if this: isEmpty() {
      return 0
    } else {
      return 1 + this: left(): size() + this: right(): size()
    }
  }

  function contains = |this, val| {
    if this: isEmpty() {
      return false
    } else {
      if this: value() == val {
        return true
      } else {
        return this: left(): contains(val) or this: right(): contains(val)
      }
    }
  }
}

let tree = Tree_Node(10,
  Tree_Node(5, Tree_Empty(), Tree_Empty()),
  Tree_Node(15, Tree_Empty(), Tree_Empty())
)

println(tree: size())          # 3
println(tree: contains(5))     # true
println(tree: contains(20))    # false

Variant-Specific Augmentation

The most advanced feature: augmenting individual variants using Union$Variant syntax.

Concept

union Something = {
  Yum = { value }
  Yuck = { value }
}

# Augment only the Yum variant
augment Something$Yum {
  function so = |this, ifYum, ifYuck| {
    return ifYum(this: value())
  }
}

# Augment only the Yuck variant
augment Something$Yuck {
  function so = |this, ifYum, ifYuck| {
    return ifYuck(this: value())
  }
}

let banana = Something.Yum("🍌")
let hotPepper = Something.Yuck("🌢️")

banana: so(
  |v| { return "Yum! " + v },
  |v| { return "Yuck! " + v }
)  # β†’ "Yum! 🍌"

Option with Variant Augmentation

union Option = { Some = { value }, None }

# General method for all variants
augment Option {
  function isDefined = |this| {
    return this: isSome()
  }
}

# Specific method for Some
augment Option$Some {
  function getOrElse = |this, default| {
    return this: value()
  }

  function map = |this, fn| {
    return Option.Some(fn(this: value()))
  }
}

# Specific method for None
augment Option$None {
  function getOrElse = |this, default| {
    return default
  }

  function map = |this, fn| {
    return this  # Does nothing
  }
}

Why Use Variant Augmentation?

  1. Polymorphism: Different behavior per variant
  2. Cleaner Code: No if statements in methods
  3. Performance: Direct dispatch to the right implementation
  4. Semantics: Each variant has its own specific methods

Result with Variant Augmentation

union Result = { Ok = { value }, Err = { message } }

# General methods
augment Result {
  function isSuccess = |this| {
    return this: isOk()
  }
}

# Ok-specific methods
augment Result$Ok {
  function unwrap = |this| {
    return this: value()
  }

  function mapValue = |this, fn| {
    return Result.Ok(fn(this: value()))
  }
}

# Err-specific methods
augment Result$Err {
  function unwrap = |this| {
    println("⚠️  Attempted to unwrap an error!")
    return null
  }

  function mapValue = |this, fn| {
    return this  # Errors are not transformed
  }
}

Complete Practical Examples

Safe Calculator

union CalcResult = { Success = { value }, Error = { message } }

augment CalcResult {
  function map = |this, fn| {
    if this: isSuccess() {
      return CalcResult.Success(fn(this: value()))
    } else {
      return this
    }
  }

  function display = |this| {
    if this: isSuccess() {
      return "βœ… " + str(this: value())
    } else {
      return "❌ " + this: message()
    }
  }
}

function safeDivide = |a, b| {
  if b == 0 {
    return CalcResult_Error("Division by zero")
  } else {
    return CalcResult_Success(a / b)
  }
}

# Usage
let result = safeDivide(100, 4)
  : map(|x| { return x * 2 })
  : map(|x| { return x + 10 })

println(result: display())  # βœ… 60

Task State Machine

union TaskState = {
  Pending
  Running = { progress }
  Completed = { result }
  Failed = { error }
}

# Each variant has its own transition methods
augment TaskState$Pending {
  function start = |this| {
    return TaskState.Running(0)
  }
}

augment TaskState$Running {
  function updateProgress = |this, newProgress| {
    if newProgress >= 100 {
      return TaskState.Completed("Task completed successfully")
    } else {
      return TaskState.Running(newProgress)
    }
  }

  function fail = |this, error| {
    return TaskState.Failed(error)
  }
}

augment TaskState$Completed {
  function restart = |this| {
    return TaskState.Pending()
  }
}

augment TaskState$Failed {
  function retry = |this| {
    return TaskState.Pending()
  }
}

# Lifecycle simulation
var task = TaskState.Pending()
task = task: start()                    # β†’ Running(0)
task = task: updateProgress(50)         # β†’ Running(50)
task = task: updateProgress(100)        # β†’ Completed
task = task: restart()                  # β†’ Pending

Best Practices

βœ… Do

  1. Use descriptive variant names

    # βœ… Clear
    union HttpResponse = { Success = { data }, Error = { message } }
    
    # ❌ Ambiguous
    union Response = { A = { x }, B = { y } }
    
  2. Combine with match for elegant code

    function handleResponse = |response| {
      return match {
        when response: isSuccess() then processData(response: data())
        when response: isError() then logError(response: message())
        otherwise "Unknown"
      }
    }
    
  3. Use auto-generated methods

    # GoloScript automatically generates: isSuccess(), isError()
    if result: isSuccess() {
      # Processing...
    }
    
  4. Augment with utility methods

    augment Result {
      function isFailure = |this| {
        return this: isErr()
      }
    
      function getOrDefault = |this, default| {
        if this: isOk() {
          return this: value()
        } else {
          return default
        }
      }
    }
    

❌ Don’t

  1. Too many variants

    # ❌ Hard to maintain
    union Status = {
      Status1, Status2, Status3, Status4,
      Status5, Status6, Status7, Status8
    }
    
    # βœ… Group logically
    union TaskStatus = { Pending, Running = { progress }, Done = { result } }
    union ErrorStatus = { NetworkError = { code }, ValidationError = { field } }
    
  2. Variants with too many fields

    # ❌ Too complex
    union User = {
      Active = { id, name, email, age, address, phone, role, dept }
    }
    
    # βœ… Use structs
    struct UserData = { id, name, email, age, address, phone, role, dept }
    union UserStatus = { Active = { data }, Inactive = { reason } }
    
  3. Ignore errors

    # ❌ Ignores errors
    let result = operation()
    println(result: value())  # Crash if it's an error!
    
    # βœ… Always check
    if result: isOk() {
      println(result: value())
    } else {
      println("Error: " + result: message())
    }
    

Comparison with Other Languages

Feature GoloScript Rust TypeScript Haskell
Union types βœ… union Result = { Ok, Err } βœ… enum Result<T,E> βœ… type Result = Ok | Err βœ… data Result = Ok | Err
Pattern matching βœ… match + isXxx() βœ… match ⚠️ Type guards βœ… case
Augmentation βœ… augment Union βœ… impl ⚠️ Prototype/class βœ… Instance
Variant aug. βœ… augment Union$Variant βœ… (via traits) ❌ βœ…
Syntax 3 syntaxes! 1 syntax 1 syntax 1 syntax

Summary

Union types in GoloScript are:

General Recommendation: Start with simple patterns (Option, Result), then explore augmentation for richer abstractions. Variant-specific augmentation is reserved for advanced cases requiring sophisticated polymorphism.

Β© 2026 GoloScript Project | Built with Gu10berg

Subscribe: πŸ“‘ RSS | βš›οΈ Atom